////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.charts.series
{

import flash.display.DisplayObject;
import flash.display.Graphics;
import flash.geom.Rectangle;
import mx.charts.HitData;
import mx.charts.chartClasses.BoundedValue;
import mx.charts.chartClasses.DataDescription;
import mx.charts.chartClasses.InstanceCache;
import mx.charts.chartClasses.LegendData;
import mx.charts.chartClasses.Series;
import mx.charts.renderers.CircleItemRenderer;
import mx.charts.series.items.BubbleSeriesItem;
import mx.charts.series.renderData.BubbleSeriesRenderData;
import mx.charts.styles.HaloDefaults;
import mx.collections.CursorBookmark;
import mx.core.ClassFactory;
import mx.core.IDataRenderer;
import mx.core.IFactory;
import mx.core.IFlexDisplayObject;
import mx.core.mx_internal;
import mx.graphics.IFill;
import mx.graphics.IStroke;
import mx.graphics.SolidColor;
import mx.graphics.Stroke;
import mx.styles.CSSStyleDeclaration;
import mx.styles.ISimpleStyleClient;
import mx.charts.chartClasses.CartesianTransform;

use namespace mx_internal;

include "../styles/metadata/FillStrokeStyles.as"
include "../styles/metadata/ItemRendererStyles.as"

/**
 *  Defines a data series for a BubbleChart control. The default itemRenderer is the CircleRenderer class.
 *  Optionally, you can define an itemRenderer for the 
 *  data series. The itemRenderer must implement the IDataRenderer interface.
 *
 *  @mxml
 *  <p>
 *  The <code>&lt;mx:BubbleSeries&gt;</code> tag inherits all the properties of its parent classes, and 
 *  the following properties:
 *  </p>
 *  <pre>
 *  &lt;mx:BubbleSeries
 *    <strong>Properties</strong>
 *    itemType=""
 *    legendData=""
 *    maxRadius=""
 *    radiusField="<i>No default</i>"
 *    renderData=""
 *    renderDataType=""
 *    xField="null"
 *    yField="null"
 *    
 *    <strong>Styles</strong>
 *    fill="<i>IFill; no default</i>"
 *    itemRenderer="<i>itemRenderer</i>"
 *    legendMarkerRenderer="<i>Defaults to series's itemRenderer</i>"
 *    stroke="<i>IStroke; no default</i>"  
 *  /&gt;
 *  </pre>
 *  
 *  @see mx.charts.BubbleChart
 *  
 *  @includeExample ../examples/BubbleChartExample.mxml
 *  
 */
public class BubbleSeries extends Series
{
    include "../../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Class constants
	//
	//--------------------------------------------------------------------------

	/**
	 *  The type of radius axis.
	 */
	public static const RADIUS_AXIS:String = "bubbleRadius";

	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  Constructor.
	 */
	public function BubbleSeries()
	{
		super();

		_instanceCache = new InstanceCache(null,this);
		_instanceCache.creationCallback = applyItemRendererProperties;
	}

	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------

	private var _renderData:BubbleSeriesRenderData;
	
	private var _instanceCache:InstanceCache;
	
	//--------------------------------------------------------------------------
	//
	//  Properties
	//
	//--------------------------------------------------------------------------

    //----------------------------------
	//  xField
    //----------------------------------

	private var _xField:String = "";
	
    [Inspectable(category="General")]
	
	/**
	 *  Specifies the field of the data provider that determines the x-axis location of 
	 *  each data point. If <code>null</code>, Flex renders the 
	 *  data points in the order that they appear in the data provider. 
	 *  
	 *  @default null
	 */
	public function get xField():String
	{
		return _xField;
	}
	
	/**
	 *  @private
	 */
	public function set xField(value:String):void
	{
		_xField = value;

		dataChanged();
	}
	
    //----------------------------------
	//  yField
    //----------------------------------

	private var _yField:String = "";
	
    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines the y-axis location of each data point. 
	 *  If <code>null</code>, the BubbleSeries assumes that the data provider is an Array of numbers, 
	 *  and uses the numbers as values for the data points. 
	 *  
	 *  @default null
	 */
	public function get yField():String
	{
		return _yField;
	}
	
	/**
	 *  @private
	 */
	public function set yField(value:String):void
	{
		_yField = value;

		dataChanged();
	}
	
    //----------------------------------
	//  radiusField
    //----------------------------------

	private var _radiusField:String = "";

    [Inspectable(category="General")]

	/**
	 *  Specifies the field of the data provider that determines the radius of each symbol, relative to the other 
	 *  data points in the chart.
	 */
	public function get radiusField():String
	{
		return _radiusField;
	}
	
	/**
	 *  @private
	 */
	public function set radiusField(value:String):void
	{
		_radiusField = value;

		dataChanged();
	}
	
    //----------------------------------
	//  maxRadius
    //----------------------------------

    [Inspectable(category="General")]
	
	/**
	 *  The radius of the largest item rendererd in this series. When you use a BubbleSeries object in a BubbleChart, the chart automatically assigns its <code>maxRadius</code> style value to this property 
	 *	on all series in the chart. When you use BubbleSeries objects in CartesianChart controls, you manage this property manually.
	 */
	public var maxRadius:Number = 50;

    //----------------------------------
	//  itemType
    //----------------------------------

	[Inspectable(environment="none")]

	/**
	 *  The subtype of ChartItem used by this series to represent individual items.
	 *  Subclasses can override and return a more specialized class if they need to store additional information in the items.
	 */
	protected function get itemType():Class
	{
		return BubbleSeriesItem;
	}
	
    //----------------------------------
	//  renderDataType
    //----------------------------------

	[Inspectable(environment="none")]

	/**
	 *  The subtype of ChartRenderData used by this series to store all data necessary to render.
	 *  Subclasses can override and return a more specialized class if they need to store additional information for rendering.
	 */
	protected function get renderDataType():Class
	{
		return BubbleSeriesRenderData;
	}

	//--------------------------------------------------------------------------
	//
	//  methods
	//
	//--------------------------------------------------------------------------
	
	/**
	 *  @inheritDoc
	 */
	override protected function updateData():void
	{
		var renderDataType:Class = this.renderDataType;
		_renderData= new renderDataType();

		_renderData.cache = [];

		var i:uint = 0;
		var itemClass:Class = itemType;

		if (cursor != null)
		{
			cursor.seek(CursorBookmark.FIRST);
			while (!cursor.afterLast)
			{
				_renderData.cache[i] = new itemClass(this,cursor.current,i);
				i++;
				cursor.moveNext();
			}
		}


		cacheIndexValues(_xField,_renderData.cache,"xValue");
		cacheDefaultValues(_yField,_renderData.cache,"yValue");
		cacheNamedValues(_radiusField,_renderData.cache,"zValue");

		super.updateData();
	}

	/**
	 *  @inheritDoc
	 */
	override protected function updateMapping():void
	{
		dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).mapCache(_renderData.cache,"xValue","xNumber",(_xField == ""));
		dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).mapCache(_renderData.cache,"yValue","yNumber");
		dataTransform.getAxis(BubbleSeries.RADIUS_AXIS).mapCache(_renderData.cache,"zValue","zNumber");


		_renderData.cache.sortOn("zNumber",Array.NUMERIC | Array.DESCENDING);		
		super.updateMapping();
	}

	/**
	 *  @inheritDoc
	 */
	override protected function updateFilter():void
	{
		if (filterData)
		{
			_renderData.filteredCache = _renderData.cache.concat();
			dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).filterCache(_renderData.filteredCache,"xNumber","xFilter");
			stripNaNs(_renderData.filteredCache,"xFilter");
			dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).filterCache(_renderData.filteredCache,"yNumber","yFilter");
			stripNaNs(_renderData.filteredCache,"yFilter");
			dataTransform.getAxis(BubbleSeries.RADIUS_AXIS).filterCache(_renderData.filteredCache,"zNumber","zFilter");
			stripNaNs(_renderData.filteredCache,"zFilter");

		}
		else
		{
			_renderData.filteredCache = _renderData.cache;
		}
		super.updateFilter();
	}

	/**
	 *  @inheritDoc
	 */
	override protected function updateTransform():void
	{
		dataTransform.transformCache(_renderData.filteredCache,"xNumber","x","yNumber","y");
		dataTransform.getAxis(BubbleSeries.RADIUS_AXIS).transformCache(_renderData.filteredCache,"zNumber","z");
		super.updateTransform();	
	}
	
	/**
	 *  @inheritDoc
	 */
	override protected function updateDisplayList(unscaledWidth:Number,
												  unscaledHeight:Number):void
	{
		super.updateDisplayList(unscaledWidth, unscaledHeight);

		var g:Graphics = graphics;
		g.clear();

		var renderData:BubbleSeriesRenderData = (transitionRenderData)? BubbleSeriesRenderData(transitionRenderData):_renderData;
				
		if (renderData == null)
			return;

		var renderCache:Array = renderData.filteredCache;
			
			
		var sampleCount:uint = renderCache.length;
		var i:uint;
		var instances:Array;
		var inst:IFlexDisplayObject;
		var rc:Rectangle;
		var v:BubbleSeriesItem;
		

		_instanceCache.factory = getStyle("itemRenderer");
		_instanceCache.count = sampleCount;			
		instances = _instanceCache.instances;

		var bSetData:Boolean = (sampleCount > 0 && (instances[0] is IDataRenderer))

		if (renderData == transitionRenderData && transitionRenderData.elementBounds)
		{
			var elementBounds:Array = renderData.elementBounds;

			for (i=0;i<sampleCount;i++)
			{
				inst = instances[i];
				v = renderCache[i];
				v.itemRenderer = inst;
				if (bSetData)
					(inst as IDataRenderer).data = v;
				rc = elementBounds[i];
				inst.move(rc.left,rc.top);
				inst.setActualSize(rc.width,rc.height);
			}			
		}
		else
		{
			for (i=0;i<sampleCount;i++)
			{
				v = renderCache[i];
				var rad:Number = v.z * maxRadius;
				if (isNaN(rad))
					continue;

				inst = instances[i];
				v.itemRenderer = inst;
				if (bSetData)
					(inst as IDataRenderer).data = v;
				inst.move(v.x-rad,v.y-rad);
				inst.setActualSize(2*rad,2*rad);

			}
		}
		
	}


	/**
	 *  Applies style properties to the specified DisplayObject. This method is the 
	 *  callback that the InstanceCache calls when it creates a new renderer.  
	 *  
	 *  @param instance The instance being created.
	 *  @param cache A reference to the instance cache itself.
	 */
	protected function applyItemRendererProperties(instance:DisplayObject,cache:InstanceCache):void
	{
		if (instance is ISimpleStyleClient)
			ISimpleStyleClient(instance).styleName = this;
	}	


	/**
	 *  @inheritDoc
	 */
	override public function describeData(dimension:String, requiredFields:uint) : Array
	{
		validateData();

		var desc:DataDescription = new DataDescription();

		var cache:Array = _renderData.cache;

		if (dimension == CartesianTransform.HORIZONTAL_AXIS)
		{
			if ((requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0)
			{
				cache = cache.concat();
				cache.sortOn("xNumber",Array.NUMERIC);		
			}
			extractMinMax(cache, "xNumber", desc, (requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0);
			if ((requiredFields & DataDescription.REQUIRED_BOUNDED_VALUES) != 0)
			{
				desc.boundedValues= [];
				desc.boundedValues.push(new BoundedValue(desc.max,0,maxRadius));
				desc.boundedValues.push(new BoundedValue(desc.min,maxRadius,0));
			}
		}
		else if (dimension == CartesianTransform.VERTICAL_AXIS)
		{
			if ((requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0)
			{
				cache = cache.concat();
				cache.sortOn("yNumber",Array.NUMERIC);		
			}
			extractMinMax(cache, "yNumber", desc, (requiredFields & DataDescription.REQUIRED_MIN_INTERVAL) != 0);
			if ((requiredFields & DataDescription.REQUIRED_BOUNDED_VALUES) != 0)
			{
				desc.boundedValues= [];
				desc.boundedValues.push(new BoundedValue(desc.max,0,maxRadius));
				desc.boundedValues.push(new BoundedValue(desc.min,maxRadius,0));
			}
		}
		else if (dimension == BubbleSeries.RADIUS_AXIS)
		{
			extractMinMax(cache,"zNumber",desc);
		}
		else
			return [];
				
		return [desc];	
	}
	

	/**
	 *  @inheritDoc
	 */
	override public function findDataPoints(x:Number, y:Number, sensitivity:Number):Array
	{
		if (interactive == false)
			return [];

		var minDist2:Number = maxRadius + sensitivity;
		minDist2 *= minDist2;
		var minItems:Array = [];
		
		var n:int;
		var i:int;		
		
		n = _renderData.filteredCache.length;
		for (i = n - 1; i >= 0; i--)
		{
			var v:BubbleSeriesItem = _renderData.filteredCache[i];			
			var dist:Number = (v.x  - x) * (v.x  - x) + (v.y - y) * (v.y -y);
			var r2:Number = v.z * maxRadius + sensitivity;
			r2 *= r2;
			if (dist > r2)
				continue;
				
			if (dist <= minDist2)
				minItems.push(v);
		}
		
		n = minItems.length;
		for (i = 0; i < n; i++)
		{
			var item:BubbleSeriesItem = minItems[i];
			var hd:HitData = new HitData(createDataID(item.index),Math.sqrt(minDist2),item.x,item.y,item);
			hd.dataTipFunction = formatDataTip;
			minItems[i] = hd;
		}

		return minItems;
	}

	private function formatDataTip(hd:HitData):String
	{
		var dt:String = "";
		var n:String = displayName;
		if (n && n != "")
		dt += "<b>" + n + "</b><BR/>";

		
		var xName:String = dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).displayName;
		if (xName != "")
			dt += "<i>" + xName+ ":</i> ";
		dt += dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).formatForScreen(BubbleSeriesItem(hd.chartItem).xValue) + "\n";

		var yName:String = dataTransform.getAxis(CartesianTransform.HORIZONTAL_AXIS).displayName;
		if (yName != "")
			dt += "<i>" + yName + ":</i> ";
		dt += dataTransform.getAxis(CartesianTransform.VERTICAL_AXIS).formatForScreen(BubbleSeriesItem(hd.chartItem).yValue) + "\n";

		var radName:String = dataTransform.getAxis(BubbleSeries.RADIUS_AXIS).displayName;
		if (radName != "")
			dt += "<i>" + radName + ":</i> ";
		dt += dataTransform.getAxis(BubbleSeries.RADIUS_AXIS).formatForScreen(BubbleSeriesItem(hd.chartItem).zValue) + "\n";
		return dt;
			
	}

	
	
	/**
	 *  @inheritDoc
	 */
	override public function get legendData() : Array
	{
		var ld:LegendData = new LegendData();
		var marker:IFlexDisplayObject;
		ld.element = this;
		var markerFactory:IFactory = getStyle("legendMarkerRenderer");
		if (markerFactory == null)
			markerFactory = getStyle("itemRenderer");
		if (markerFactory) 
		{
			marker = markerFactory.newInstance();
			if (marker as ISimpleStyleClient)
				(marker as ISimpleStyleClient).styleName = this;
		}
		ld.marker = marker;
		ld.label = displayName;	
		ld.aspectRatio = 1;

		return [ld];
	}

	/**
	 *  @inheritDoc
	 */
	override public function styleChanged(styleProp : String) : void
	{
		super.styleChanged(styleProp);
		var styles:String = "stroke fill";
		if (styleProp == null || styleProp == "" || styles.indexOf(styleProp) != -1)
		{
			invalidateDisplayList();
			legendDataChanged();
		}
	}

	/**
	 *  @inheritDoc
	 */
	override protected function get renderData():Object
	{
		if (_renderData == null)
		{
			var renderDataType:Class = this.renderDataType;
			return new renderDataType([],[]);
		}
			
		return _renderData;
	}
	
	/**
	 *  @inheritDoc
	 */
	override public function getElementBounds(renderData:Object):void
	{
		var cache :Array = renderData.cache;
		var rb : Array = [];
		
		var sampleCount:uint = cache.length;		

		var maxBounds:Rectangle 

		if (sampleCount == 0)
		{
			maxBounds = new Rectangle(0,0,unscaledWidth,unscaledHeight);
		}
		else
		{
			var v:Object = cache[0];
			maxBounds= new Rectangle(v.x,v.y,0,0);
			var rad:Number = v.z * maxRadius;//v.radius;
			for (var i:uint=0;i<sampleCount;i++)
			{
				v = cache[i];
				rad = v.z * maxRadius;//v.radius;
				var b:Rectangle = new Rectangle(v.x-rad,v.y-rad,2*rad,2*rad);
				maxBounds.left = Math.min(maxBounds.left,b.left);
				maxBounds.top = Math.min(maxBounds.top,b.top);
				maxBounds.right = Math.max(maxBounds.right,b.right);
				maxBounds.bottom = Math.max(maxBounds.bottom,b.bottom);
				rb[i] = b;
			}
		}

		
		renderData.elementBounds = rb;
		renderData.bounds =  maxBounds;
	}

	/**
	 *  @inheritDoc
	 */
	override public function beginInterpolation(sourceRenderData:Object,destRenderData:Object):Object
	{
		var idata:Object = initializeInterpolationData(
			sourceRenderData.cache, destRenderData.cache,
			{ x: true, y: true, z: true }, itemType);

		var interpolationRenderData:BubbleSeriesRenderData = BubbleSeriesRenderData(_renderData.clone());

		interpolationRenderData.cache = idata.cache;	
		interpolationRenderData.filteredCache = idata.cache;	
		
		transitionRenderData = interpolationRenderData;
		return idata;
	}

	/**
	 *  @inheritDoc
	 */
	override protected function getMissingInterpolationValues(sourceProps:Object,srcCache:Array,destProps:Object,destCache:Array,index:Number,customData:Object):void
	{
		for (var propName:String in sourceProps)
		{
			var src:Number = sourceProps[propName];
			var dst:Number = destProps[propName];

			if (propName == "x" || propName == "y")	
				{
					if (isNaN(src))
					{
						src = dst;
					}
					if (isNaN(dst))
					{
						dst = src;
					}
			}
			else if (propName == "z")
			{
				if (isNaN(src))
				{
					src = 0;
				}
				if (isNaN(dst))
				{
					dst = 0;
				}
			}
			sourceProps[propName] = src;
			destProps[propName] = dst;
		}		
	}

	mx_internal function get items():Array
	{
		return (_renderData)? _renderData.filteredCache : null;
	}

	//--------------------------------------------------------------------------
	//
	//  Class initialization
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private static var stylesInited:Boolean = initStyles();	
	
	/**
	 *  @private
	 */
	private static function initStyles():Boolean
	{
		HaloDefaults.init();
		
		var seriesStyle:CSSStyleDeclaration =
			HaloDefaults.createSelector("BubbleSeries");		
		
		seriesStyle.defaultFactory = function():void
		{
			this.fill = new SolidColor(0x444444);
			this.itemRenderer = new ClassFactory(CircleItemRenderer);
			this.stroke = new Stroke(0, 1, 0.2);
		}

		return true;
	}
}

}
